package javango.contrib.hibernate;

import java.io.Serializable;
import java.lang.reflect.InvocationTargetException;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;

import org.apache.commons.beanutils.ConvertUtilsBean;
import org.apache.commons.beanutils.PropertyUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.hibernate.Criteria;
import org.hibernate.cfg.Configuration;
import org.hibernate.criterion.Expression;
import org.hibernate.mapping.Component;
import org.hibernate.mapping.PersistentClass;
import org.hibernate.mapping.Property;
import org.hibernate.mapping.Value;
import org.hibernate.metadata.ClassMetadata;
import org.hibernate.type.AssociationType;
import org.hibernate.type.CollectionType;
import org.hibernate.type.Type;

public class ModelDaoHelper {
	private final static Log log = LogFactory.getLog(ModelDaoHelper.class);
	
	protected HibernateUtil hibernateUtil;
	protected Class<?> modelClass;
	
	public ModelDaoHelper(HibernateUtil hibernateUtil, Class modelClass) {
		this.hibernateUtil = hibernateUtil;
		this.modelClass = modelClass;
	}

	/**
	 * Returns a the keys Class[] 
	 * @return
	 * @throws DaoException
	 */
	public Class[] getPkClass() throws DaoException {
		Configuration cfg = hibernateUtil.getConfiguration();
		PersistentClass pclass = cfg.getClassMapping(modelClass.getName());

		if (pclass == null) {
			throw new DaoException("Unable to find class : "
					+ modelClass.toString());
		}

		Property componentProperty = pclass.getIdentifierProperty();
		if (componentProperty == null) {
			Component component = pclass.getIdentifierMapper();
			if (component == null) {
				throw new DaoException("Unable to get pk mapping");
			}
			if (log.isDebugEnabled()) log.debug(String.format("Found %d keys for model %s", component.getPropertySpan(), modelClass.getName()));

			Class<?>[] classArray = new Class[component.getColumnSpan()];
			Iterator<Property> ite = component.getPropertyIterator();
			int i = 0;
			while (ite.hasNext()) {
				Property p = ite.next();
				if (log.isDebugEnabled()) log.debug("property name: " + p.getName());
				classArray[i++] = p.getType().getReturnedClass();
			}
			return classArray;
		} else {

			Value value = componentProperty.getValue();
			if (value == null)
				throw new DaoException("Component value is null");
			else {
				if (log.isDebugEnabled()) log.debug(String.format("Found simple key for model %s '%s'",
						modelClass.getName(), value.getType()
								.getReturnedClass()));
				return new Class[] { value.getType().getReturnedClass() };
			}
		}
	}

	public java.lang.reflect.Field findField(Class modelClass, String property) throws NoSuchFieldException {
		// TODO dry,  this is repeated in ModelForm,  maybe helper/util class..
		Class classToTry = modelClass;
		while(classToTry != null) {
			try {
				return classToTry.getDeclaredField(property);
			} catch (NoSuchFieldException e) {
				classToTry = classToTry.getSuperclass();
			}
		}
		return modelClass.getDeclaredField(property); // let this throw the excpetion,  i guess we could do so ourselves...
	}
	
	private ConvertUtilsBean convertUtilsBean = new ConvertUtilsBean();
	public Object convert(String value, Class clazz) {		
		return convertUtilsBean.convert(value, clazz);
	}
	
	public Object correctType(String fieldName, Object value) throws DaoException {
		return correctType(fieldName, value, this.modelClass);
	}
	
	public Object correctType(String fieldName, Object value, Class clazz) throws DaoException {
		
		try {
			// TODO May still need to convert non-strings to the correct value (ie long to double, etc).
			if (!(value instanceof String)) {
				return value;
			}		
			ClassMetadata metadata = hibernateUtil.getSessionFactory().getClassMetadata(clazz);
			if (metadata != null) {
				Type type = metadata.getPropertyType(fieldName);
				if (type instanceof CollectionType) {
					log.warn("Unable to handle collection: " + fieldName);
					return value;
				} else if (type instanceof AssociationType) {
					AssociationType assocType = (AssociationType)type;
					Class[] assocPkClass = getPkClass();
					if (assocPkClass.length > 1) {
						log.error("Multi keyed associations are not supported in correctType");
						return value;
					}
					Object o = convert((String)value, assocPkClass[0]);
					return hibernateUtil.getSession().get(modelClass, (Serializable)o);
					// TODO if return value is null should something be done??
					//return value;
				}				
			}
				
			java.lang.reflect.Field field = findField(clazz, fieldName);
			return convert((String)value, field.getType());

		} catch (NoSuchFieldException e) {
			log.error(e,e);
			throw new DaoException(e);
		}
	}
	
	public Object convertToPk(Object pk) throws DaoException {
	if (pk == null) return null;
		
		Class[] keyClass = getPkClass();
		if (keyClass.length != 1) {
			throw new DaoException("get(pk) not supported for composite keyed models");
		}
				
		if (pk.getClass().equals(keyClass[0])) {
			return pk;
		}		
		
		if (log.isDebugEnabled()) log.debug("Trying to convert input to type " + keyClass[0]);
		
		return convert(pk.toString(), keyClass[0]);
	}

	public void processField(Criteria criteria, String fieldName, Object value, String alias, Class aliasClass) throws DaoException {

		int qualifierLocation = fieldName.indexOf("__");
		if (qualifierLocation == -1) {
			criteria.add(Expression.eq(fieldName,  correctType(fieldName, value)));
			return;
		}
		String qualifier = fieldName.substring(qualifierLocation+2);
		fieldName = fieldName.substring(0,qualifierLocation);

		Object searchValue = null;
		if (alias != null) {			
			searchValue = correctType(fieldName, value, aliasClass);
			fieldName = alias + "." + fieldName;
		} else {
			searchValue = correctType(fieldName, value);
		}
		
		 
		
		if ("like".equals(qualifier)) {
			criteria.add(Expression.like(fieldName, searchValue));
		} else if ("ilike".equals(qualifier)) {
			criteria.add(Expression.ilike(fieldName, searchValue));
		} else if ("lte".equals(qualifier)) {
			criteria.add(Expression.le(fieldName, searchValue));
		} else if ("gte".equals(qualifier)) {
			criteria.add(Expression.ge(fieldName, searchValue));
		} else if ("lt".equals(qualifier)) {
			criteria.add(Expression.lt(fieldName, searchValue));
		} else if ("gt".equals(qualifier)) {
			criteria.add(Expression.gt(fieldName, searchValue));
		} else if ("eq".equals(qualifier)) {
			criteria.add(Expression.eq(fieldName, searchValue));
		} else if ("ne".equals(qualifier)) {
			criteria.add(Expression.ne(fieldName, searchValue));
		} else if ("date".equals(qualifier)) {
			if (!(searchValue instanceof Date) || searchValue == null) {
				throw new DaoException ("__date search requires a non-null instance of java.util.Date");
			}
			Date date = (Date)searchValue;
			
			Calendar from = new GregorianCalendar();
			from.setTime(date);
			from.set(Calendar.HOUR_OF_DAY, 0);
			from.set(Calendar.MINUTE, 0);
			from.set(Calendar.SECOND, 0);

			Calendar to = new GregorianCalendar();
			to.setTime(date);
			to.set(Calendar.HOUR_OF_DAY, 23);
			to.set(Calendar.MINUTE, 59);
			to.set(Calendar.SECOND, 59);
			
			criteria.add(Expression.between(fieldName, from.getTime(), to.getTime()));
		} else {
			throw new DaoException ("Unknown search type '" + qualifier + "'");
		}
	}
	
	public void updateCriteriaFromMap(Criteria criteria, Map<String, Object> params, String alias, Class aliasClass) throws DaoException {
		if (params == null) return;
		
		for(Entry<String, Object> entry : params.entrySet()) {
			if (entry.getValue() != null) processField(criteria, entry.getKey(), entry.getValue(), alias, aliasClass);
		}
	}
	
	public void updateCriteria(Criteria criteria, Object params) throws DaoException {
		updateCriteria(criteria, params, null, null);		
	}
	
	@SuppressWarnings("unchecked")
	public void updateCriteria(Criteria criteria, Object params, String alias, Class aliasClass) throws DaoException {
		if (alias != null) {
			criteria.createAlias(alias, alias);
		}
		
		if (params == null) return;
		
		if (params instanceof Map) {
			updateCriteriaFromMap(criteria, (Map)params, alias, aliasClass);
			return;			
		}
		
		try {
			Class c = params.getClass();
			java.lang.reflect.Field[] fields = c.getDeclaredFields();
			for (int i=0; i<fields.length; i++) {
				String fieldName = fields[i].getName();
				Object value = PropertyUtils.getProperty(params, fieldName);
				if (value != null) processField(criteria, fieldName, value, alias, aliasClass);
			}			
		} catch (IllegalAccessException e) {
			log.error(e,e);
		} catch (InvocationTargetException e) {
			log.error(e,e);
		} catch (NoSuchMethodException e) {
			log.error(e,e);
		}	
	}
	
	/**
	 * Returns a the keys Class[] 
	 * @return
	 * @throws DaoException
	 */
	public String getPkProperty() throws DaoException {
		Configuration cfg = hibernateUtil.getConfiguration();
		PersistentClass pclass = cfg.getClassMapping(modelClass.getName());

		if (pclass == null) {
			throw new DaoException("Unable to find class : "
					+ modelClass.toString());
		}

		Property componentProperty = pclass.getIdentifierProperty();
		if (componentProperty == null) {
			throw new UnsupportedOperationException("Multiple primary keys not supported");
//			Component component = pclass.getIdentifierMapper();
//			if (component == null) {
//				throw new DaoException("Unable to get pk mapping");
//			}
//			if (log.isDebugEnabled()) log.debug(String.format("Found %d keys for model %s", component.getPropertySpan(), modelClass.getName()));
//
//			Class<?>[] classArray = new Class[component.getColumnSpan()];
//			Iterator<Property> ite = component.getPropertyIterator();
//			int i = 0;
//			while (ite.hasNext()) {
//				Property p = ite.next();
//				if (log.isDebugEnabled()) log.debug("property name: " + p.getName());
//				classArray[i++] = p.getType().getReturnedClass();
//			}
//			return classArray;
		} else {
			return componentProperty.getName();
		}
	}
}
